package edu.nyu.msis.parser;

import edu.nyu.msis.ds.MachineInstruction;
import edu.nyu.msis.ds.Module;
import edu.nyu.msis.ds.Program;
import edu.nyu.msis.ds.SymbolTable;
import edu.nyu.msis.utilities.FileIO;
import java.util.HashMap;
import java.util.Vector;
import java.util.Iterator;
import java.util.Map;

public class MainParser {
	FileIO myFileReader;
	StateMachine stateMachine;
    SymbolTable symTable;
    Program program;
    Vector<String> errors;
    Vector<String> warnings;

	/**
	 * 
	 */
	public MainParser() {
		super();
		myFileReader = new FileIO();
		stateMachine = new StateMachine();
		symTable = new SymbolTable();
		program = new Program();
		errors = new Vector<String>();
		warnings = new Vector<String>();
	}

	/**
	 * @param myFileReader
	 */
	public MainParser(FileIO myFileReader) {
		super();
		stateMachine = new StateMachine();
		symTable = new SymbolTable();
		program = new Program();
		errors = new Vector<String>();
		warnings = new Vector<String>();
		this.myFileReader = myFileReader;
	}

	public void runLoop () {
		ParserStates parserState = ParserStates.MODULE_BEGIN;
		Module module = new Module(); //create a temp module
		stateMachine.setModule(module); //tell the state machine to use this module to fill up the data
		Character readChar = myFileReader.readOneChar();
		
		while (readChar!='\0') {
			parserState = stateMachine.parse(readChar);
			if (parserState == ParserStates.MODULE_END) { //reached the end of module
				program.addModule(module);
				module = new Module(); //start saving into a new module
				stateMachine.setModule(module);
			}
			else if (parserState == ParserStates.ERROR_STATE) {
				System.out.println("Invalid input! Cannot continue...");
				return;
			}
			readChar = myFileReader.readOneChar();
		}//end of while
		if (readChar=='\0') {
			parserState = stateMachine.parse(readChar);
			if (parserState == ParserStates.MODULE_END) { //reached the end of module
				program.addModule(module);
			}
			else if (parserState == ParserStates.ERROR_STATE) {
				System.out.println("Invalid input! Cannot continue...");
				return;
			}
		}
		
		boolean result = runFirstPass();
		if (result) { //only run the second pass if first pass had no errors
			System.out.println("First pass done!");
			result = runSecondPass();
		}
		if (result) {
			System.out.println("Second pass done!");
			//System.out.println(symTable.getMyTable());
			myFileReader.printSymbolTable(symTable);
			myFileReader.printProgram(program);
			myFileReader.printErrors("warning", warnings);
			System.out.println("Done!");
		}
		else {
			myFileReader.printErrors("error", errors);
			myFileReader.printErrors("warning", warnings);
		}
		myFileReader.closeFiles();
	}
	
	public boolean runFirstPass()
	{
		int numOfModules = program.getNumOfModules();
		Module m1 = null;
		Integer baseAddress, moduleIndex;
		baseAddress = 0;
		moduleIndex = 0;
		
		while (moduleIndex < numOfModules) {
			m1 = program.getModuleByIndex(moduleIndex);
			// Set the module start and end addresses first
			if (setModuleLimits(m1, baseAddress)) { //if setting of limits was successful go ahead
				baseAddress += m1.getSize();
				// Now save the definitions in the symbol table
				if (!addModuleDefsToSymbolTable(m1)) { //if adding of definitions was not success
					return false;
				}
			}
			else {
				return false; //setting of limits was not successful
			}
			moduleIndex++;
		}
		return true; //reached here means all success in pass-1
	} //End of runFirstPass
	
	private boolean setModuleLimits(Module module, Integer baseAddress) {
		Integer endAddress = baseAddress + module.getSize() - 1;
		module.setStartAddress(baseAddress);
		if (program.isAddressWithinLimit(endAddress)) {
			module.setEndAddress(endAddress);
			return true;
		}
		else {
			return false;
		}
	}
	
	private boolean addModuleDefsToSymbolTable (Module module) {
		HashMap<String,Integer> tempVars = null;
		Integer add;
		String varName;
		tempVars = module.getDefList().getDefinedVars();
		for (Iterator<Map.Entry<String, Integer>> i= tempVars.entrySet().iterator(); i.hasNext(); ) {
			Map.Entry<String, Integer> entry= i.next();
			add = module.getStartAddress() + entry.getValue();
			varName = entry.getKey();
			if (symTable.isSymbolPresent(varName)) {
				errors.add(varName+" multiply defined!\n");
				return false;
			}
			else {
				if (module.isAddressWithinLimit(add)) {
					symTable.addToSymbolTable(varName, add);
				}
				else {
					errors.add(varName+" exceeds the limit of module of size"+module.getSize());
					return false;
				}
			}
		}
		return true;
	}
	
	public boolean runSecondPass(){
		int numOfModules= program.getNumOfModules();
		Module m1 = null;
		Integer	moduleIndex = 0;

		checkDefinedButNotUsed(); //not an irrecoverable error condition. just a warning

		while (moduleIndex < numOfModules) {
			m1 = program.getModuleByIndex(moduleIndex);
			if (checkUseVarsPresentInSymTable(m1)) { //Check if used but not defined
				if (!resolveAllAddresses(m1)) {
					return false;
				}
			}
			else {
				return false; //irrecoverable error. a variable is used but not defined.
			}
			moduleIndex++;
		}
		return true;
	}//End of runSecondPass
	
	private boolean checkUseVarsPresentInSymTable(Module module) {
		Vector<String> tempUsedVars = null;
		Integer numOfVars = 0;
		String varName = "";
		
		tempUsedVars = module.getUseList().getUsedVars();
		numOfVars = tempUsedVars.size();
		for(Integer i = 0; i < numOfVars; i++)
		{
			varName = tempUsedVars.get(i);
			if (!symTable.isSymbolPresent(varName)) {
				errors.add(varName+" used in module but not defined!\n");
				return false;
			}
		}//To check each variable of Use List
		return true;
	}

	private void checkDefinedButNotUsed() {
		Vector<String> tempUsedVars = null;
		HashMap<String,Integer> symbolTableVars;
		String varName,symbolName;
		Integer moduleIndex,numberOfVars;	
		Boolean flag = false;
		Module m1;
		int numOfModules= program.getNumOfModules();
		//////////////////////////////////////////////////////////////////////////////////////
		//loop to check  symbol defined but not used
		symbolTableVars = symTable.getMyTable();

		for (Iterator<Map.Entry<String, Integer>> i= symbolTableVars.entrySet().iterator(); i.hasNext(); )
		{
			Map.Entry<String, Integer> entry= i.next();
			symbolName  = entry.getKey();
			moduleIndex = 0;
			while (!flag && (moduleIndex < numOfModules))
			{
				m1 = program.getModuleByIndex(moduleIndex);	
				tempUsedVars = m1.getUseList().getUsedVars();
				numberOfVars = tempUsedVars.size();
				for(Integer n = 0; n < numberOfVars; n++)
				{
						varName = tempUsedVars.get(n);
						if(varName.equals(symbolName))
							flag = true;
				}//end of for loop for UseList
				moduleIndex++;
			}//end of while loop for Module
			if(!flag){
				warnings.add(symbolName+" was defined but never used!");
			}
			flag = false;
		 }// End of for loop for Symbol Table values
	}
	
	private boolean resolveAllAddresses(Module module) {
		Vector<MachineInstruction> tempInstructions = module.getInstructions();
		Iterator<MachineInstruction> it = tempInstructions.iterator();
		Integer numOfUsedVars =  module.getUseList().getSize();
		int[] tempArr = new int[numOfUsedVars]; //create a temp array of size = numberOfUsedVars
		
		for (int i=0; i<numOfUsedVars; i++) {
			tempArr[i] = 0; //set all values of temp array to 0
		}
		//Loop over all the instructions of the module
		while (it.hasNext()) {
			MachineInstruction instruct = it.next();
			switch (instruct.getType()) {
				case IMMEDIATE:
					break; // nothing to do
				case ABSOLUTE:
					if (!program.isAddressWithinLimit(instruct.getAddress())) {
						errors.add(instruct.getAddress()+" refers to an invalid machine address");
						return false; // exit now.
					}
					break;
				case RELATIVE: //relocate the relative addresses
					Integer newAddress = instruct.getAddress() + module.getStartAddress();
					if (!module.isAddressWithinLimit(newAddress)) { //check if the address is within limits of the module
						errors.add(instruct.getAddress()+" exceeds the limit of module of size "+ module.getSize());
						return false;
					}
					else {
						instruct.setAddress(newAddress);
					}
					break;
				case EXTERNAL:
					Integer newAddress2 = instruct.getAddress();
					if (newAddress2 > module.getUseList().getSize()) { //check if the external address exceeds the number of used variables
						errors.add(newAddress2+" exceeds the size of use list of module");
						return false;
					}
					else {
						//if it is an E address set the corresponding index of tempArray to 1
						//to indicate that we have found the reference of this var of the use list in the src code
						tempArr[newAddress2] = 1;
						String variable = module.getUseList().getUsedVars().elementAt(newAddress2);
						newAddress2 = symTable.getMyTable().get(variable);
						instruct.setAddress(newAddress2);
					}
					break;
			} //end of switch instruction-type
		} //end of while loop over all instructions
		for (int i=0; i<numOfUsedVars; i++) {
			if (tempArr[i] == 0) { // if it is still 0 means this variable is not present in the src
				warnings.add(module.getUseList().getUsedVars().elementAt(i)+" is never used in the module");
			}
		}
		return true;
	} //End of resolveAllAdresses
}//End of class